{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 2\n",
    "Автор материала: программист-исследователь Mail.ru Group, старший преподаватель Факультета Компьютерных Наук ВШЭ Юрий Кашницкий. Материал распространяется на условиях лицензии [Creative Commons CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# <center>Тема 10. Бустинг\n",
    "## <center>Часть 9. Xgboost и несбалансированные выборки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Общие советы\n",
    "Есть несколько общих советов по работе с несбалансированными выборками:\n",
    "\n",
    "- собрать больше данных\n",
    "- использовать метрики, нечувствительные к дисбалансу классов (F1, ROC AUC)\n",
    "- oversampling/undersampling - брать больше объектов мало представленного класса, и мало - частого класса\n",
    "- создать искусственные объекты, похожие на объекты редкого класса (например, алгоритмом SMOTE)\n",
    "\n",
    "С XGBoost можно:\n",
    "- следить за тем, чтобы параметр `min_child_weight` был  мал, хотя по умолчанию он и так равен 1. \n",
    "- задать ббольше веса некоторым объектам при инициализации `DMatrix`\n",
    "- контролировать отшошение числа представителей разных классов с помощью параметра `set_pos_weight`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Подготовка данных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import xgboost as xgb\n",
    "from sklearn.datasets import make_classification\n",
    "from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.preprocessing import LabelEncoder"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Сгенерируем несбалансированную выборку для задачи классификации.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X, y = make_classification(\n",
    "    n_samples=200,\n",
    "    n_features=5,\n",
    "    n_informative=3,\n",
    "    n_classes=2,\n",
    "    weights=[0.9, 0.1],\n",
    "    shuffle=True,\n",
    "    random_state=123,\n",
    ")\n",
    "\n",
    "print(\"There are {} positive instances.\".format(y.sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Разбиваем на обучающую и тестовую выборки. Соблюдаем стратификацию.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "    X, y, test_size=0.33, stratify=y, random_state=123\n",
    ")\n",
    "\n",
    "print(\"Train set labels distibution: {}\".format(np.bincount(y_train)))\n",
    "print(\"Test set labels distibution:  {}\".format(np.bincount(y_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**В начале игнорируем то, что выборка несбалансированная.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dtrain = xgb.DMatrix(X_train, label=y_train)\n",
    "dtest = xgb.DMatrix(X_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Инициализируем параметры Xgboost - будем обучать композицию из 15 \"пеньков\".**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "params = {\"objective\": \"binary:logistic\", \"max_depth\": 1, \"silent\": 1, \"eta\": 1}\n",
    "\n",
    "num_rounds = 15"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xgb_model = xgb.train(params, dtrain, num_rounds)\n",
    "y_test_preds = (xgb_model.predict(dtest) > 0.5).astype(\"int\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Матрица ошибок.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(\n",
    "    pd.Series(y_test, name=\"Actual\"),\n",
    "    pd.Series(y_test_preds, name=\"Predicted\"),\n",
    "    margins=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Доля правильных ответов, точность и полнота.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Accuracy: {0:.2f}\".format(accuracy_score(y_test, y_test_preds)))\n",
    "print(\"Precision: {0:.2f}\".format(precision_score(y_test, y_test_preds)))\n",
    "print(\"Recall: {0:.2f}\".format(recall_score(y_test, y_test_preds)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Видно, что полнота низкая. то есть алгоритм плохо распознает объекты мало представленного класса. Если интересно находить как раз такие редкие объекты, то от такого алгоритма мало толку.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Задание весов вручную\n",
    "**При создании объекта `DMatrix` можно сразу явно указать, что вес положительных объектов в 5 раз больше, чем отрицательных.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "weights = np.zeros(len(y_train))\n",
    "weights[y_train == 0] = 1\n",
    "weights[y_train == 1] = 5\n",
    "\n",
    "dtrain = xgb.DMatrix(X_train, label=y_train, weight=weights)  # weights added\n",
    "dtest = xgb.DMatrix(X_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Повторим обучение модели, как и в предыдущем случае.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xgb_model = xgb.train(params, dtrain, num_rounds)\n",
    "y_test_preds = (xgb_model.predict(dtest) > 0.5).astype(\"int\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(\n",
    "    pd.Series(y_test, name=\"Actual\"),\n",
    "    pd.Series(y_test_preds, name=\"Predicted\"),\n",
    "    margins=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Accuracy: {0:.2f}\".format(accuracy_score(y_test, y_test_preds)))\n",
    "print(\"Precision: {0:.2f}\".format(precision_score(y_test, y_test_preds)))\n",
    "print(\"Recall: {0:.2f}\".format(recall_score(y_test, y_test_preds)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Видим, что вес объектов надо настраивать в зависимости от задачи.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Параметр `scale_pos_weight` в Xgboost\n",
    "**Задание весов вручную можно заменить на параметр `scale_pos_weight`**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dtrain = xgb.DMatrix(X_train, label=y_train)\n",
    "dtest = xgb.DMatrix(X_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Инициализируем параметр `scale_pos_weight` соотношением числа отрицательных и положительных объектов.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_labels = dtrain.get_label()\n",
    "\n",
    "ratio = float(np.sum(train_labels == 0)) / np.sum(train_labels == 1)\n",
    "params[\"scale_pos_weight\"] = ratio"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xgb_model = xgb.train(params, dtrain, num_rounds)\n",
    "y_test_preds = (xgb_model.predict(dtest) > 0.5).astype(\"int\")\n",
    "\n",
    "pd.crosstab(\n",
    "    pd.Series(y_test, name=\"Actual\"),\n",
    "    pd.Series(y_test_preds, name=\"Predicted\"),\n",
    "    margins=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Accuracy: {0:.2f}\".format(accuracy_score(y_test, y_test_preds)))\n",
    "print(\"Precision: {0:.2f}\".format(precision_score(y_test, y_test_preds)))\n",
    "print(\"Recall: {0:.2f}\".format(recall_score(y_test, y_test_preds)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**В этом случае значение параметра `scale_pos_weight` надо выбирать в зависимости от желаемого соотношения между точностью и полнотой.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Пример с оттоком клиентов телеком-компании"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Загрузим данные и осуществим минимальную предобработку.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv(\"../../data/telecom_churn.csv\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Штаты просто занумеруем, а признаки International plan (наличие международного роуминга), Voice mail plan (наличие голосовой почтыы) и целевой Churn сделаем бинарными.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "state_enc = LabelEncoder()\n",
    "df[\"State\"] = state_enc.fit_transform(df[\"State\"])\n",
    "df[\"International plan\"] = (df[\"International plan\"] == \"Yes\").astype(\"int\")\n",
    "df[\"Voice mail plan\"] = (df[\"Voice mail plan\"] == \"Yes\").astype(\"int\")\n",
    "df[\"Churn\"] = (df[\"Churn\"]).astype(\"int\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Видим, что соотношение хороших и плохих клиентов примерно 6:1.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[\"Churn\"].value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Разделим данные на обучающую и тестовую выборки в отношении 7:3 с учетом соотношения классов. Инициализируем соотв. объекты DMatrix dtrain и dtest.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "    df.drop(\"Churn\", axis=1),\n",
    "    df[\"Churn\"],\n",
    "    test_size=0.3,\n",
    "    stratify=df[\"Churn\"],\n",
    "    random_state=42,\n",
    ")\n",
    "dtrain = xgb.DMatrix(X_train, y_train)\n",
    "dtest = xgb.DMatrix(X_test, y_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "params = {\"objective\": \"binary:logistic\", \"max_depth\": 4, \"silent\": 1, \"eta\": 0.3}\n",
    "\n",
    "num_rounds = 100"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xgb_model = xgb.train(params, dtrain, num_rounds)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "preds_prob = xgb_model.predict(dtest)\n",
    "pred_labels = (preds_prob > 0.5).astype(\"int\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(\n",
    "    pd.Series(dtest.get_label(), name=\"Actual\"),\n",
    "    pd.Series(pred_labels, name=\"Predicted\"),\n",
    "    margins=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Accuracy: {0:.2f}\".format(accuracy_score(dtest.get_label(), pred_labels)))\n",
    "print(\"Precision: {0:.2f}\".format(precision_score(dtest.get_label(), pred_labels)))\n",
    "print(\"Recall: {0:.2f}\".format(recall_score(dtest.get_label(), pred_labels)))\n",
    "print(\"F1: {0:.2f}\".format(f1_score(dtest.get_label(), pred_labels)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Теперь изменим параметр `scale_pos_weight` и проделаем то же самое.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "params[\"scale_pos_weight\"] = 10"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xgb_model = xgb.train(params, dtrain, num_rounds)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "preds_prob = xgb_model.predict(dtest)\n",
    "pred_labels = (preds_prob > 0.5).astype(\"int\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(\n",
    "    pd.Series(dtest.get_label(), name=\"Actual\"),\n",
    "    pd.Series(pred_labels, name=\"Predicted\"),\n",
    "    margins=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Accuracy: {0:.2f}\".format(accuracy_score(dtest.get_label(), pred_labels)))\n",
    "print(\"Precision: {0:.2f}\".format(precision_score(dtest.get_label(), pred_labels)))\n",
    "print(\"Recall: {0:.2f}\".format(recall_score(dtest.get_label(), pred_labels)))\n",
    "print(\"F1: {0:.2f}\".format(f1_score(dtest.get_label(), pred_labels)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Видим, что таким образом мы настроили модель так, что она меньше ошибается в распознавании плохих клиентов.**"
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
